package jsr292.weaver.gen;

import static org.objectweb.asm.Opcodes.AALOAD;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.*;

import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;

import jsr292.java.lang.invoke.MethodHandle;
import jsr292.java.lang.invoke.MethodType;
import jsr292.weaver.Capabilities;
import jsr292.weaver.OnlineWeaver;
import jsr292.weaver.Utils;
import jsr292.weaver.Wrapper;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.CheckClassAdapter;

public class TrampolineGenerator {
    private static final String TRAMPOLINE_PACKAGE = TrampolineGenerator.class.getPackage().getName()+'.';
    
    public static MethodHandle createATrampoline(Class<?> declaringClass, int methodModifiers, String methodName, MethodType signature, boolean isStatic) {
        Class<?> clazz = getTrampolineClass(declaringClass, methodModifiers, methodName, signature, isStatic);
        
        try {
            return (MethodHandle)clazz.getConstructor(MethodType.class).newInstance(signature);
        } catch (NoSuchMethodException e) {
            throw new AssertionError(e);
        } catch (InstantiationException e) {
            throw new AssertionError(e);
        } catch (IllegalAccessException e) {
            throw new AssertionError(e);
        } catch (InvocationTargetException e) {
            throw new AssertionError(e.getCause());
        } 
    }
    
    private static Class<?> getTrampolineClass(Class<?> declaringClass, int methodModifiers, String methodName, MethodType signature, boolean isStatic) {
        ClassLoader classLoader = declaringClass.getClassLoader();
        if (classLoader == null) { 
            classLoader = ClassLoader.getSystemClassLoader();
        }
        
        // use the classloader cache as a cache :)
        String mangledTrampolineName = mangleClassName(declaringClass, methodName, signature);
        try {
            return classLoader.loadClass(mangledTrampolineName);
        } catch (ClassNotFoundException e) {
            byte[] array = generateByteArray(mangledTrampolineName.replace('.', '/'), declaringClass, methodModifiers, methodName, signature, isStatic);
           
            //DEBUG
            //CheckClassAdapter.verify(new ClassReader(array), classLoader, true, new PrintWriter(System.out));
            
            return Capabilities.define(classLoader, mangledTrampolineName, array);
        }
    }
    
    private static String mangleClassName(Class<?> declaringClass, String methodName, MethodType signature) {
        StringBuilder builder = new StringBuilder();
        builder.append(TRAMPOLINE_PACKAGE);
        
        // don't use String.replace(String,String) because it has a dependency to java.util.regex
        //builder.append(className.replace(".", "$$"));
        
        String className = declaringClass.getName();
        for(int i=0; i<className.length(); i++) {
            char c = className.charAt(i);
            if (c == '.') {
                builder.append("$$");
            } else {
                builder.append(c);
            }
        }
        
        builder.append("$$").append(methodName).append("$$");
        mangleSignature(builder, signature);
        return builder.toString();
    }
    
    private static StringBuilder mangleSignature(StringBuilder builder, MethodType signature) {
        for(int i=0; i<signature.parameterCount(); i++) {
            mangleType(builder, signature.parameterType(i)).append("$$");
        }
        return mangleType(builder, signature.returnType());
    }
    
    private static StringBuilder mangleType(StringBuilder builder, Class<?> clazz) {
        while (clazz.isArray()) {
            builder.append('$');
            clazz = clazz.getComponentType();
        }
        return builder.append(clazz.getName().replace('.', '_'));
    }
    
    //FIXME should compute max by hand
    private static byte[] generateByteArray(String internalClassName, Class<?> declaringClass, int methodModifiers, String methodName, MethodType signature, boolean isStatic) {
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS /*|ClassWriter.COMPUTE_FRAMES*/);
        writer.visit(V1_5, ACC_SUPER|ACC_PUBLIC, internalClassName, null, Utils.JSR292_METHOD_HANDLE, null);
        
        // constructor, delegating to MethodHandle constructor
        MethodVisitor init = writer.visitMethod(ACC_PUBLIC, "<init>", '(' + Utils.JSR292_METHODTYPE_DESC + ")V", null, null);
        init.visitCode();
        init.visitVarInsn(ALOAD, 0);
        init.visitVarInsn(ALOAD, 1);
        init.visitMethodInsn(INVOKESPECIAL, Utils.JSR292_METHOD_HANDLE, "<init>", '(' + Utils.JSR292_METHODTYPE_DESC + ")V");
        init.visitInsn(RETURN);
        init.visitMaxs(-1, -1);
        init.visitEnd();
        
        // method $invoke$
        int parameterCount = signature.parameterCount();
        MethodVisitor invoke = writer.visitMethod(ACC_PUBLIC, "$invoke$", OnlineWeaver.invokeMethodDescriptor(parameterCount), null, null);
        invoke.visitCode();
        
        // unbox parameters
        boolean arrayUnbox = (parameterCount > Utils.PARAMETER_THRESHOLD);
        for(int i=0; i<parameterCount; i++) {
            if (arrayUnbox) {
                invoke.visitVarInsn(ALOAD, 1);
                Utils.ldcInteger(invoke, i);
                invoke.visitInsn(AALOAD);
            } else {
                invoke.visitVarInsn(ALOAD, i + 1);
            }
            
            Class<?> parameterType = signature.parameterType(i);
            if (parameterType == Object.class)
                continue;
            
            if (parameterType.isPrimitive()) {
                Wrapper wrapper = Wrapper.getWrapper(parameterType);
                String wrapperClassName = wrapper.wrapperClassName;
                invoke.visitTypeInsn(CHECKCAST, wrapperClassName);
                invoke.visitMethodInsn(INVOKEVIRTUAL, wrapperClassName, wrapper.unboxingMethodName, wrapper.unboxingDesc);
            } else {
                invoke.visitTypeInsn(CHECKCAST, parameterType.getName().replace('.', '/'));
            }
        }
        
        int opcode;
        if (isStatic) {
            opcode = INVOKESTATIC;
        } else {
            opcode = (declaringClass.isInterface())?INVOKEINTERFACE:INVOKEVIRTUAL;
            signature = signature.dropParameterTypes(0, 1);
        }
        String methodDescriptor = signature.toMethodDescriptorString();
        invoke.visitMethodInsn(opcode,
                declaringClass.getName().replace('.', '/'),
                methodName,
                methodDescriptor);
        
        // box return type
        Class<?> returnType = signature.returnType();
        if (returnType == void.class) {
            invoke.visitInsn(ACONST_NULL);
        } else {
            if (returnType.isPrimitive()) {  // box
                Wrapper wrapper = Wrapper.getWrapper(returnType);
                invoke.visitMethodInsn(INVOKESTATIC, wrapper.wrapperClassName, "valueOf", wrapper.boxingDesc);
            }
        }
        invoke.visitInsn(ARETURN);
        
        invoke.visitMaxs(-1, -1);
        invoke.visitEnd();
        
        
        // method isWeavable
        MethodVisitor isWeavable = writer.visitMethod(ACC_PUBLIC, "isWeavable", "(Ljava/lang/Class;)Z", null, null);
        isWeavable.visitCode();
        
        isWeavable.visitVarInsn(ALOAD, 1);
        Utils.ldcInteger(isWeavable, methodModifiers);
        isWeavable.visitLdcInsn(Type.getType(declaringClass));
        isWeavable.visitMethodInsn(INVOKESTATIC, Utils.JSR292_ACCESS, "isVisible",
                "(Ljava/lang/Class;ILjava/lang/Class;)Z");
        isWeavable.visitInsn(IRETURN);
        
        isWeavable.visitMaxs(-1, -1);
        isWeavable.visitEnd();
        
        
        // method weave
        MethodVisitor weave = writer.visitMethod(ACC_PUBLIC, "weave", "(Ljsr292/weaver/opt/OptimizerContext;Ljsr292/weaver/opt/OptimizerVarAllocator;)V", null, null);
        weave.visitCode();
        
        weave.visitVarInsn(ALOAD, 1);
        weave.visitVarInsn(ALOAD, 2);
        weave.visitLdcInsn(Type.getInternalName(declaringClass));
        weave.visitLdcInsn(methodName);
        weave.visitLdcInsn(methodDescriptor);
        weave.visitInsn((isStatic)?ICONST_1:ICONST_0);
        weave.visitMethodInsn(INVOKESTATIC, "jsr292/weaver/opt/OptimizerSnippets",
                "trampolineMethod",
                "(Ljsr292/weaver/opt/OptimizerContext;Ljsr292/weaver/opt/OptimizerVarAllocator;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V");
        weave.visitInsn(RETURN);
        
        weave.visitMaxs(-1, -1);
        weave.visitEnd();
        
        writer.visitEnd();
        
        return writer.toByteArray();
    }
}
